//
//  NMDataStore.swift
//  NetEaseMusic
//
//  Created by SAGESSE on 2020/5/29.
//  Copyright © 2020 SAGESSE. All rights reserved.
//

import UIKit


///
/// A Data Store.
///
class NMDataStore<Key, Value> where Key: Hashable {
    
    /// Every data item.
    class Item {
        
        fileprivate var storage: Value?
        fileprivate var accessor: Accessor = .init()
        fileprivate var references: NSHashTable<Reference> = .init(options: .opaqueMemory)
        
        fileprivate let key: Key
        fileprivate unowned let store: NMDataStore

        fileprivate init(_ key: Key, store: NMDataStore) {
            self.key = key
            self.store = store
        }

        var value: Value? {
            get {
                // Call user getter.
                if let getter = accessor.get {
                    return getter()
                }
                return storage
            }
            set {
                // Call user willSet.
                accessor.willSet?(newValue)
                references.allObjects.forEach {
                    $0.accessor.willSet?(newValue)
                }
                
                // The old value copy only when old value is use.
                var oldValue: Value?
                if useOldValue {
                    oldValue = value
                }
                
                // Call user setter.
                if accessor.set?(newValue) == nil {
                    storage = newValue
                    store.sync(key, with: newValue)
                }
                
                // Call user didSet.
                accessor.didSetOfFast?()
                accessor.didSet?(oldValue)
                references.allObjects.forEach {
                    $0.accessor.didSetOfFast?()
                    $0.accessor.didSet?(oldValue)
                }
            }
        }
        
        fileprivate var useOldValue: Bool {
            if accessor.didSet != nil {
                return true
            }
            return references.allObjects.contains {
                return $0.accessor.didSet != nil
            }
        }

        fileprivate func clean(_ key: Key) {
            // When item is empty, must remove the item in context.
            guard references.count == 0 && accessor.get == nil && accessor.set == nil && accessor.willSet == nil && accessor.didSet == nil && storage == nil else {
                return
            }
            store.items.removeValue(forKey: key)
        }
    }
    class Accessor {
        
        var set: ((Value?) -> ())?
        var get: (() -> (Value?))?

        var willSet: ((Value?) -> ())?
        var didSet: ((Value?) -> ())?
        
        /// This is a quick callback without oldValue.
        var didSetOfFast: (() -> ())?
    }
    class Reference {
        
        fileprivate weak var item: Item?
        fileprivate weak var store: NMDataStore?
        fileprivate lazy var accessor: Accessor = .init()
        fileprivate var isLinking: Bool = false

        fileprivate init(_ key: Key, store: NMDataStore) {
            self.key = key
            self.store = store
        }
        deinit {
            item?.references.remove(self)
            item?.clean(key)
        }
        
        let key: Key
        var value: Value? {
            get { ii?.value }
            set { ir?.value = newValue }
        }
        
        func willSet(_ handler: @escaping () -> ()) {
            link()
            accessor.willSet = { _ in
                handler()
            }
        }
        func willSet(_ handler: @escaping (Value?) -> ()) {
            link()
            accessor.willSet = {
                handler($0)
            }
        }
        
        func didSet(_ handler: @escaping () -> ()) {
            link()
            accessor.didSetOfFast = handler
        }
        func didSet(_ handler: @escaping (Value?) -> ()) {
            link()
            accessor.didSet = {
                handler($0)
            }
        }
        
        private var ii: Item? {
            if let newValue = item {
                return newValue
            }
            let newValue = store?.items[key]
            item = newValue
            return newValue
        }
        private var ir: Item? {
            if let newValue = item {
                return newValue
            }
            let newValue = store?.item(key)
            item = newValue
            return newValue
        }
        
        private func link() {
            if !isLinking {
                isLinking = true
                ir?.references.add(self)
            }
        }
    }
    
    
    subscript (key: Key) -> Value? {
        get { items[key]?.value }
        set {
            let ref = item(key)
            ref.value = newValue
            ref.clean(key)
        }
    }
    
    
    func get(_ keys: Key..., body: @escaping () -> (Value?)) {
        assign(keys, at: \.get, body: body)
    }

    func set(_ keys: Key..., body: @escaping () -> ()) {
        assign(keys, at: \.set) { _ in
            body()
        }
    }
    func set(_ keys: Key..., body: @escaping (Value?) -> ()) {
        assign(keys, at: \.set, body: body)
    }
    
    func willSet(_ keys: Key..., body: @escaping () -> ()) {
        assign(keys, at: \.willSet) { _ in
            body()
        }
    }
    func willSet(_ keys: Key..., body: @escaping (Value?) -> ()) {
        assign(keys, at: \.willSet, body: body)
    }
    
    func didSet(_ keys: Key..., body: @escaping () -> ()) {
        assign(keys, at: \.didSetOfFast, body: body)
    }
    func didSet(_ keys: Key..., body: @escaping (Value?) -> ()) {
        assign(keys, at: \.didSet, body: body)
    }
    
    
    func sync(_ body: @escaping (Key, Value?) -> ()) {
        syn = body
    }
    func clean() {
        items.removeAll()
    }
    
    
    func reference(for key: Key) -> Reference {
        return Reference(key, store: self)
    }

    private func item(_ key: Key) -> Item {
        if let item = items[key] {
            return item
        }
        let newValue = Item(key, store: self)
        items[key] = newValue
        return newValue
    }
    private func assign<T>(_ keys: [Key], at keyPath: ReferenceWritableKeyPath<Accessor, T?>, body: T) {
        keys.forEach {
            item($0).accessor[keyPath: keyPath] = body
        }
    }
    
    private func sync(_ key: Key, with value: Value?) {
        syn?(key, value)
    }
    
    private var syn: ((Key, Value?) -> Void)?
    private var items: [Key: Item] = [:]
}
